//
//  MZApi.swift
//  MZTool
//
//  Created by gby on 2023/2/6.
//

import Foundation
import Alamofire




public extension MZMethod {
    var supportsMultipart: Bool {
        switch self {
        case .post, .put, .patch, .connect:
            return true
        default:
            return false
        }
    }
}




public protocol MZApiType: AnyObject {
    associatedtype Target: MZApiRequest
}

open class MZApi<Target: MZApiRequest>: MZApiType{

    public var plugins: [MZPluginType]
    
    public let session: Session
    
    let callbackQueue: DispatchQueue?
    
    
    public init(plugins: [MZPluginType],
                sesson: Session = MZApi.defaultAlamofireSession(),
                callbackQueue: DispatchQueue? = nil) {
        self.plugins = plugins
        self.callbackQueue = callbackQueue
        self.session = sesson
    }
    
    open func endpoint(_ token: MZApiRequest) -> MZEndpoint{
        MZApi.defaultEndpointMapping(for: token as! Target)
    }
    
    @discardableResult
    public func request(_ api: Target,
                        callbackQueue: DispatchQueue? = .none,
                        progress: MZProgressBlock? = nil,
                        closure: @escaping (Result<MZApiResponse, MZError>) -> Void) -> MZCancellable {
        let callbackqueue = callbackQueue ?? self.callbackQueue
        return self.mutableRequest(target: api, callbackQueue: callbackqueue, progress: progress, completion: closure)
    }
    
//    @discardableResult
//    public func subRequest(_ api: Target,
//                        callbackQueue: DispatchQueue? = .none,
//                        progress: MZProgressBlock?,
//                        closure: @escaping (Result<MZApiResponse, Error>) -> Void) -> MZCancellable {
//        let cancelable = MZCancellableWrapper()
//        return cancelable
//    }
}


private extension MZApi{
    
    func mutableRequest(target: Target, callbackQueue: DispatchQueue?, progress: MZProgressBlock?, completion: @escaping MZCompletionClosure) -> MZCancellable {
        
        let cancelable = MZCancellableWrapper()
        let endpoint = self.endpoint(target)
        
        let completionClosure: MZCompletionClosure = { res in
            let processdResult = self.plugins.reduce(res) { $1.process($0, target: target) }
            completion(processdResult)
        }
        
        let networkingClosure = { (resuestResult: Result<URLRequest, MZError>) in
            if cancelable.isCancelled {
                self.cancelCompletion(completion, target: target)
                return
            }
            var request: URLRequest!
            switch  resuestResult{
            case .success(let urlRequest):
                request = urlRequest
            case .failure(let error):
                completionClosure(.failure(error))
                return
            }
            cancelable.innserCancellable = self.performRequest(target: target, requset: request, callbackQueue: callbackQueue, progress, completion: completionClosure, endpoint: endpoint)
        }
        MZApi.defaultRequestMapping(for: endpoint, closure: networkingClosure)
        return cancelable
    }
    
    
    
    func performRequest (target: Target, requset: URLRequest, callbackQueue: DispatchQueue? , _ progress: MZProgressBlock?, completion: @escaping MZCompletionClosure, endpoint: MZEndpoint) -> MZCancellable{
        switch endpoint.task {
        case .requestPlain, .requestData, .requestJSONEncodable, .requestCustomJSONEncodable, .requestParameter, .requestCompositeData, .requestCompositeParameters:
            return self.sendRequest(target: target, requset: requset, callbackQueue: callbackQueue, progress, completion: completion, endpoint: endpoint)
        case .uploadFile(let url):
            return self.uploadFile(target: target, requset: requset, callbackQueue: callbackQueue, file: url, progress, completion: completion)
        case .uploadMultipart(let multipartBody), .uploadCompositeMultipart(let multipartBody, urlParameters: _):
            guard !multipartBody.isEmpty && endpoint.method.supportsMultipart else {
                fatalError("\(target) is not a multipart upload target")
            }
            return self.uploadMultipart(target: target, requset: requset, callbackQueue: callbackQueue, multipartBody: multipartBody, progress, completion: completion)
        case .downloadDestination(let destination), .downloadParameters(_ , _ , let destination):
            return self.downloadRequest(target: target, requset: requset, callbackQueue: callbackQueue, destination: destination, progress, completion: completion)
        }
    }
    
    func cancelCompletion(_ completion: MZCompletionClosure, target: Target) {
        let error = MZError.underlying(NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil), nil)
        plugins.forEach { $0.didReceive(.failure(error), target: target) }
        completion(.failure(error))
    }
    
    
    
    //MARK: 网络请求
    func sendRequest(target: Target, requset: URLRequest, callbackQueue: DispatchQueue? , _ progress: MZProgressBlock?, completion: @escaping MZCompletionClosure, endpoint: MZEndpoint) -> MZCancellable{
        let interceptor = self.interceptor(target: target)
        let initialRequest: DataRequest = session.requestQueue.sync {
            let initialRequest = session.request(requset, interceptor: interceptor)
            setup(interceptor: interceptor, with: target, and: initialRequest)
            return initialRequest
        }
        let validationCodes = target.validationType.statusCodes
        let alamoRequest = validationCodes.isEmpty ? initialRequest : initialRequest.validate(statusCode: validationCodes)
        return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
    }
    
    
    //MARK: 上传data
    func uploadMultipart(target: Target, requset: URLRequest, callbackQueue: DispatchQueue?, multipartBody: [MZMultipartFormData], _ progress: MZProgressBlock?, completion: @escaping MZCompletionClosure) -> MZCancellable {
        let formData = MZRequestMultipartFormData()
        formData.applyMZMultipartFormData(multipartBody)
        
        let interceptor = self.interceptor(target: target)
        let uploadRequest: UploadRequest = session.requestQueue.sync {
            let uploadRequest = session.upload(multipartFormData: formData, with: requset, interceptor: interceptor)
            setup(interceptor: interceptor, with: target, and: uploadRequest)
            return uploadRequest
        }
        let validationCodes = target.validationType.statusCodes
        let alamoRequest = validationCodes.isEmpty ? uploadRequest : uploadRequest.validate(statusCode: validationCodes)
        return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
    }
    
    //MARK: 上传文件
    func uploadFile(target: Target, requset: URLRequest, callbackQueue: DispatchQueue?, file: URL, _ progress: MZProgressBlock?, completion: @escaping MZCompletionClosure) -> MZCancellable {
        let interceptor = self.interceptor(target: target)
        let uploadRequest: UploadRequest = session.requestQueue.sync {
            let uploadRequest = session.upload(file, with: requset, interceptor: interceptor)
            setup(interceptor: interceptor, with: target, and: uploadRequest)
            return uploadRequest
        }
        let validationCodes = target.validationType.statusCodes
        let alamoRequest = validationCodes.isEmpty ? uploadRequest : uploadRequest.validate(statusCode: validationCodes)
        return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
    }
    
    
    //MARK: 下载
    func downloadRequest(target: Target, requset: URLRequest, callbackQueue: DispatchQueue?, destination: @escaping MZDownloadDestination, _ progress: MZProgressBlock?, completion: @escaping MZCompletionClosure) -> MZCancellable{
        let interceptor = self.interceptor(target: target)
        let downloadRequest: DownloadRequest = session.requestQueue.sync {
            let downloadRequest = session.download(requset, interceptor: interceptor, to: destination)
            setup(interceptor: interceptor, with: target, and: downloadRequest)
            return downloadRequest
        }
        let validationCodes = target.validationType.statusCodes
        let alamoRequest = validationCodes.isEmpty ? downloadRequest : downloadRequest.validate(statusCode: validationCodes)
        return sendAlamofireRequest(alamoRequest, target: target, callbackQueue: callbackQueue, progress: progress, completion: completion)
    }
    
    
    func sendAlamofireRequest<T> (_ alamoRequest: T, target: Target, callbackQueue: DispatchQueue?, progress progressCompletion: MZProgressBlock?, completion: @escaping MZCompletionClosure) -> MZCancellable where T: MZRequest, T: MZRequestable {
        
        let plugins = self.plugins
        var progressAlamoRequest = alamoRequest
        let progressClosure: (Progress) -> Void = { progress in
            let sendProgress: () -> Void = {
                progressCompletion?(MZProgressResponse(progress: progress))
            }
            
            if let callbackQueue = callbackQueue {
                callbackQueue.async(execute: sendProgress)
            }else{
                sendProgress()
            }
        }
        
        if progressCompletion != nil {
            switch progressAlamoRequest{
            case let downloadRequest as MZDownloadRequest:
                if let downloadRequest = downloadRequest.downloadProgress(closure: progressClosure) as? T {
                    progressAlamoRequest = downloadRequest
                }
            case let uploadRequest as MZUploadRequest:
                if let uploadRequest = uploadRequest.uploadProgress(closure: progressClosure) as? T {
                    progressAlamoRequest = uploadRequest
                }
            case let dataRequest as MZDataRequest:
                if let dataRequest = dataRequest.downloadProgress(closure: progressClosure) as? T {
                    progressAlamoRequest = dataRequest
                }
            default:break
            }
        }
        
        let completionHandler: MZRequestableCompletion = { response, request, data, error in
            let result = convertResponseToResult(response, request: request, data: data, error: error)
            plugins.forEach { $0.didReceive(result, target: target) }
            if let progressCompletion = progressCompletion {
                let value = try? result.get()
                switch progressAlamoRequest {
                case let downloadRequest as MZDownloadRequest:
                    progressCompletion(MZProgressResponse(progress: downloadRequest.downloadProgress, response: value))
                case let uploadRequest as MZUploadRequest:
                    progressCompletion(MZProgressResponse(progress: uploadRequest.uploadProgress, response: value))
                case let dataRequest as MZDataRequest:
                    progressCompletion(MZProgressResponse(progress: dataRequest.downloadProgress, response: value))
                default:
                    progressCompletion(MZProgressResponse(response: value))
                }
            }
            completion(result)
        }
        
        progressAlamoRequest = progressAlamoRequest.response(callbackQueue: callbackQueue, completionHandler: completionHandler)
        progressAlamoRequest.resume()
        return MZCancellableToken(request: progressAlamoRequest)
    }
}






public extension MZApi{
    
    final class func defaultEndpointMapping(for target: Target) -> MZEndpoint {
        MZEndpoint(
            url: URL(target: target).absoluteString,
            method: target.method,
            task: target.task,
            httpHeaderFields: target.headers
        )
    }
    
    final class func defaultRequestMapping(for endpoint: MZEndpoint, closure: MZRequestResultClosure) {
        do {
            let urlRequest = try endpoint.urlRequest()
            closure(.success(urlRequest))
        } catch MZError.requestMapping(let url) {
            closure(.failure(MZError.requestMapping(url)))
        } catch MZError.parameterEncoding(let error) {
            closure(.failure(MZError.parameterEncoding(error)))
        } catch {
            closure(.failure(MZError.underlying(error, nil)))
        }
    }
    
    final class func defaultAlamofireSession() -> Session {
        let configuration = URLSessionConfiguration.default
        configuration.headers = .default

        return Session(configuration: configuration, startRequestsImmediately: false)
    }
}






extension MZApi{
    
    private func interceptor(target: Target) -> MZRequestInterceptor {
        return MZRequestInterceptor(prepare: { [weak self] urlRequest in
            return self?.plugins.reduce(urlRequest) { $1.prepare($0, target: target) } ?? urlRequest
       })
    }

    private func setup(interceptor: MZRequestInterceptor, with target: Target, and request: Request) {
        interceptor.willSend = { [weak self, weak request] urlRequest in
            guard let self = self, let request = request else { return }

            let stubbedAlamoRequest = MZRequestTypeWrapper(request: request, urlRequest: urlRequest)
            self.plugins.forEach { $0.willSend(stubbedAlamoRequest, target: target) }
        }
    }
}


public func convertResponseToResult(_ response: HTTPURLResponse?, request: URLRequest?, data: Data?, error: Swift.Error?) ->
    Result<MZApiResponse, MZError> {
        switch (response, data, error) {
        case let (.some(response), data, .none):
            let response = MZApiResponse(statusCode: response.statusCode, data: data ?? Data(), request: request, response: response)
            return .success(response)
        case let (.some(response), _, .some(error)):
            let response = MZApiResponse(statusCode: response.statusCode, data: data ?? Data(), request: request, response: response)
            let error = MZError.underlying(error, response)
            return .failure(error)
        case let (_, _, .some(error)):
            let error = MZError.underlying(error, nil)
            return .failure(error)
        default:
            let error = MZError.underlying(NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil), nil)
            return .failure(error)
        }
}
